package org.infodavid.common.scheduler.impl;

import java.io.InputStream;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.UUID;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.joda.time.DateTime;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.simpl.RAMJobStore;

import org.infodavid.common.scheduler.PersistenceHelper;
import org.infodavid.common.scheduler.SchedulerService;
import org.infodavid.common.scheduler.commands.IScheduledCommand;
import org.infodavid.common.scheduler.impl.commands.SchedulerCommandImpl;
import org.infodavid.common.util.ArraysUtil;
import org.infodavid.common.util.CollectionsUtil;

/**
 * The Class QuartzNotifierServiceImpl.
 */
@SuppressWarnings({
  "unchecked"
})
public class SchedulerServiceImpl extends SchedulerService {

  /** The Constant log. */
  private static final Log LOGGER = LogFactory.getLog(SchedulerServiceImpl.class);

  /** The Constant COMMAND_KEY. */
  protected static final String COMMAND_KEY = "command";

  /** The scheduler. */
  private final Scheduler scheduler;

  /**
   * Instantiates a new scheduler service impl.
   * @param configFilename the config filename
   * @param dataSourceJndiName the data source jndi name
   * @throws Exception the exception
   */
  public SchedulerServiceImpl(final String configFilename, final String dataSourceJndiName)
      throws Exception {
    super();

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Loading properties from file: " + configFilename
          + " and initializing scheduler");
    }

    final StdSchedulerFactory factory = new StdSchedulerFactory();
    final Properties properties = new Properties();
    InputStream input = null;
    String value;

    try {
      input = getClass().getResourceAsStream(configFilename);

      properties.load(input);

      value = properties.getProperty("org.quartz.jobStore.class");

      if (!(value == null || value.equals(RAMJobStore.class.getName()))) {
        PersistenceHelper.getInstance().initializeDataSource(dataSourceJndiName);
      }

      factory.initialize(properties);
    }
    finally {
      if (input != null) {
        try {
          input.close();
        }
        catch (final Exception e) {
          LOGGER.debug("An error occurs while closing the input stream on properties file", e);
        }
      }
    }

    scheduler = factory.getScheduler();

    if (!scheduler.isStarted()) {
      scheduler.start();
    }
  }

  /**
   * Creates the trigger.
   * @param date the date
   * @param delayInMs the delay in ms
   * @param name the job name
   * @return the trigger
   */
  private Trigger createTrigger(final DateTime date, final Long delayInMs, final String name) {
    final Trigger trigger;
    final String group = getGroupName();

    if (date == null && delayInMs == null) {
      trigger = new SimpleTrigger(name, group);
    }
    else {
      Date start;

      if (date == null) {
        start = new Date();
      }
      else {
        start = date.toDate();
      }

      if (delayInMs == null) {
        trigger = new SimpleTrigger(name, group, start, null, 0, 0L);
      }
      else {
        trigger =
            new SimpleTrigger(name, group, start, null, SimpleTrigger.REPEAT_INDEFINITELY,
                delayInMs.longValue());
      }
    }

    if (trigger != null) {
      trigger.setJobName(name);
      trigger.setJobGroup(group);
    }

    return trigger;
  }

  /**
   * Schedule job.
   * @param date the date
   * @param delayInMs the delay in ms
   * @param cmd the notifier command
   * @return the trigger
   * @throws SchedulerException the scheduler exception
   */
  private Trigger scheduleJob(final DateTime date, final Long delayInMs, final IScheduledCommand cmd)
      throws SchedulerException {
    final String group = getGroupName();
    String name = cmd.getKey();

    if (StringUtils.isEmpty(name)) {
      name = UUID.randomUUID().toString();
    }

    final Trigger trigger = createTrigger(date, delayInMs, name);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Using job fqn: " + group + '.' + name);
    }

    final JobDetail jobDetail = new JobDetail(name, group, SchedulerJob.class);

    jobDetail.getJobDataMap().put(COMMAND_KEY, cmd);
    jobDetail.setRequestsRecovery(cmd.isRecoveryEnabled());
    jobDetail.setVolatility(false);
    jobDetail.setDurability(false);

    scheduler.scheduleJob(jobDetail, trigger);

    if (LOGGER.isDebugEnabled()) {
      if (delayInMs == null) {
        LOGGER.debug("Job " + group + '.' + name + " scheduled at " + trigger.getStartTime());
      }
      else {
        LOGGER.debug("Job " + group + '.' + name + " scheduled at " + trigger.getStartTime()
            + " every " + delayInMs + "ms");
      }
    }

    return trigger;
  }

  /**
   * Reschedule job.
   * @param date the date
   * @param delayInMs the delay in ms
   * @param cmd the notifier command
   * @return the trigger
   * @throws SchedulerException the scheduler exception
   */
  private Trigger rescheduleJob(final DateTime date, final Long delayInMs,
      final IScheduledCommand cmd) throws SchedulerException {
    final String group = getGroupName();
    String name = cmd.getKey();

    if (StringUtils.isEmpty(name)) {
      name = UUID.randomUUID().toString();
    }

    Trigger trigger = createTrigger(date, delayInMs, name);

    if (LOGGER.isDebugEnabled()) {
      LOGGER.debug("Using job fqn: " + group + '.' + name);
    }

    scheduler.rescheduleJob(name, group, trigger);

    if (LOGGER.isDebugEnabled()) {
      if (delayInMs == null) {
        LOGGER.debug("Job " + group + '.' + name + " rescheduled at " + trigger.getStartTime());
      }
      else {
        LOGGER.debug("Job " + group + '.' + name + " rescheduled at " + trigger.getStartTime()
            + " every " + delayInMs + "ms");
      }
    }

    return trigger;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#newCommandInstance(java .lang.Class)
   */
  @Override
  public IScheduledCommand newCommandInstance() {
    return new SchedulerCommandImpl();
  }

  /**
   * Gets the scheduler.
   * @return the scheduler
   */
  public Scheduler getScheduler() {
    return scheduler;
  }

  /**
   * Gets the group name.
   * @return the group name
   */
  public String getGroupName() {
    return getClass().getPackage().getName();
  }

  /**
   * Gets the command.
   * @param data the data
   * @return the command
   */
  private IScheduledCommand getCommand(final JobDataMap data) {
    IScheduledCommand cmd;

    if (data == null || data.isEmpty()) {
      cmd = null;
    }
    else {
      final Object value = data.get(COMMAND_KEY);
      cmd = value instanceof IScheduledCommand ? (IScheduledCommand)value : null;

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Command: " + cmd);
      }
    }

    return cmd;
  }

  private void validate(IScheduledCommand cmd) throws SchedulerException {
    if (cmd == null) {
      throw new SchedulerException("Command is null");
    }

    if (cmd.getKey() == null || cmd.getKey().trim().length() == 0) {
      cmd.setKey(String.valueOf(cmd.hashCode()));

      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Command key is null, using hash code: " + cmd.getKey());
      }
    }

    if (cmd.getProcessorClass() == null) {
      throw new SchedulerException("Processor class is null");
    }
    else {
      try {
        if (cmd.getProcessorClass().getConstructor() == null) {
          throw new SchedulerException("Processor constructor is not valid");
        }
      }
      catch (Exception e) {
        try {
          if (cmd.getProcessorClass().getDeclaredConstructor() == null) {
            throw new SchedulerException("Processor constructor is not valid");
          }
        }
        catch (Exception e2) {
          LOGGER.warn("Processor constructor is not valid", e);

          throw new SchedulerException("Processor constructor is not valid");
        }
      }
    }
  }

  /**
   * Schedule.
   * @param date the date
   * @param delayInMs the delay in ms
   * @param commands the commands
   * @throws SchedulerException the scheduler exception
   */
  protected void schedule(final DateTime date, final Long delayInMs,
      final IScheduledCommand... commands) throws SchedulerException {
    if (commands == null || commands.length == 0) {
      throw new SchedulerException("No valid command");
    }

    for (final IScheduledCommand cmd : commands) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Scheduling command: " + cmd);
      }

      validate(cmd);

      try {
        if (cmd.getExpirationDate() != null && cmd.getExpirationDate().isBeforeNow()) {
          LOGGER.warn("Command expired: " + cmd.getKey());
        }
        else if (date == null || date.isBefore(System.currentTimeMillis())) {
          scheduleJob(null, delayInMs, cmd);
        }
        else {
          scheduleJob(date, delayInMs, cmd);
        }
      }
      catch (final Throwable e) {
        LOGGER.error("An error occurs while processing command: " + cmd, e);
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#schedule(org.joda.time. DateTime,
   * org.infodavid.common.scheduler.commands.IScheduledCommand[])
   */
  @Override
  public void schedule(final DateTime date, final IScheduledCommand... commands)
      throws SchedulerException {
    schedule(date, null, commands);
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#reschedule(org.joda.time .DateTime,
   * org.infodavid.common.scheduler.commands.IScheduledCommand[])
   */
  @Override
  public void reschedule(DateTime start, IScheduledCommand... commands) throws SchedulerException {
    reschedule(start, null, commands);
  }

  /**
   * Reschedule.
   * @param date the date
   * @param delayInMs the delay in ms
   * @param commands the commands
   * @throws SchedulerException the scheduler exception
   */
  protected void reschedule(final DateTime date, final Long delayInMs,
      final IScheduledCommand... commands) throws SchedulerException {
    if (commands == null || commands.length == 0) {
      throw new SchedulerException("No valid command");
    }

    for (final IScheduledCommand cmd : commands) {
      if (LOGGER.isDebugEnabled()) {
        LOGGER.debug("Rescheduling command: " + cmd);
      }

      validate(cmd);

      try {
        if (cmd.getExpirationDate() != null && cmd.getExpirationDate().isBeforeNow()) {
          LOGGER.warn("Command expired: " + cmd.getKey());
        }
        else if (date == null || date.isBefore(System.currentTimeMillis())) {
          rescheduleJob(null, delayInMs, cmd);
        }
        else {
          rescheduleJob(date, delayInMs, cmd);
        }
      }
      catch (final Throwable e) {
        LOGGER.error("An error occurs while processing command: " + cmd, e);
      }
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#schedule(org.joda.time. DateTime, long,
   * org.infodavid.common.scheduler.commands.IScheduledCommand[])
   */
  @Override
  public void schedule(final DateTime date, final long delayInMs,
      final IScheduledCommand... commands) throws SchedulerException {
    schedule(date, Long.valueOf(delayInMs), commands);
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#listRunning(int, int, java.util.List)
   */
  public void listRunning(final int start, final int size, final List<IScheduledCommand> results)
      throws SchedulerException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    List<JobExecutionContext> contexts;

    try {
      contexts = scheduler.getCurrentlyExecutingJobs();

      if (contexts != null) {
        contexts = CollectionsUtil.getInstance().subList(contexts, start, size);
        IScheduledCommand cmd;

        for (final JobExecutionContext item : contexts) {
          if (isDebugEnabled) {
            LOGGER.debug("Searching command: " + item.getTrigger().getName());
          }

          cmd = getCommand(item.getJobDetail().getJobDataMap());

          if (cmd != null) {
            if (isDebugEnabled) {
              LOGGER.debug("Command found: " + cmd);
            }

            results.add(cmd);
          }
        }
      }
    }
    catch (final SchedulerException e) {
      handle(e, "An error occurs while retrieving commands", this);
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#listScheduled(int, int, java.util.List)
   */
  public void listScheduled(final int start, final int size, final List<IScheduledCommand> results)
      throws SchedulerException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    final String groupName = getGroupName();
    String[] triggerNames;

    try {
      triggerNames = scheduler.getTriggerNames(groupName);

      if (triggerNames != null) {
        triggerNames = ArraysUtil.getInstance().subArray(triggerNames, start, size);
        IScheduledCommand cmd;
        Trigger trigger;

        for (final String item : triggerNames) {
          if (isDebugEnabled) {
            LOGGER.debug("Searching trigger: " + item);
          }

          trigger = scheduler.getTrigger(item, groupName);

          if (trigger != null) {
            if (isDebugEnabled) {
              LOGGER.debug("Searching command: " + item);
            }

            cmd = getCommand(trigger.getJobDataMap());

            if (cmd != null) {
              if (isDebugEnabled) {
                LOGGER.debug("Command found: " + cmd);
              }

              results.add(cmd);
            }
          }
        }
      }
    }
    catch (final SchedulerException e) {
      handle(e, "An error occurs while retrieving commands", this);
    }
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#getCommand(java.lang.String )
   */
  public IScheduledCommand getCommand(final String key) throws SchedulerException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();
    IScheduledCommand result = null;

    if (key == null) {
      if (isDebugEnabled) {
        LOGGER.debug("No specified command key");
      }
    }
    else {
      try {
        final JobDetail job = scheduler.getJobDetail(key, getGroupName());

        result = job == null ? null : getCommand(job.getJobDataMap());
      }
      catch (final SchedulerException e) {
        handle(e, "An error occurs while cancelling commands", this);
      }
    }

    return result;
  }

  /*
   * (non-Javadoc)
   * @see org.infodavid.common.scheduler.SchedulerService#cancel(java.lang.String[])
   */
  public void cancel(final String... keys) throws SchedulerException {
    final boolean isDebugEnabled = LOGGER.isDebugEnabled();

    if (keys == null || keys.length == 0) {
      if (isDebugEnabled) {
        LOGGER.debug("No specified command key");
      }
    }
    else {
      final String groupName = getGroupName();

      try {
        for (final String item : keys) {
          scheduler.unscheduleJob(item, groupName);
          scheduler.deleteJob(item, groupName);
        }
      }
      catch (final SchedulerException e) {
        handle(e, "An error occurs while cancelling commands", this);
      }
    }
  }
}
